package first.group.ssh.channel;

import first.group.ssh.ConnectionCallback;
import first.group.ssh.packet.ChannelClosePacket;
import first.group.ssh.packet.ChannelDataPacket;
import first.group.ssh.packet.ChannelEofPacket;
import first.group.ssh.packet.ChannelFailurePacket;
import first.group.ssh.packet.ChannelOpenPacket;
import first.group.ssh.packet.ChannelRequestPacket;
import first.group.ssh.packet.ChannelSuccessPacket;
import first.group.ssh.packet.ChannelWindowAdjustPacket;
import first.group.ssh.readBuffer.ReadBuffer;
import first.group.ssh.util.ByteBufferUtil;
import first.group.ssh.writebuffer.WriteBuffer;
import first.group.ssh.writebuffer.WriteContent;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

public abstract class Channel implements ChannelCallback {

    private static final Logger logger = Logger.getLogger("LocalChannel");

    private final int localMaxPacketSize;
    private int remoteMaxPacketSize = -1;
    private ConnectionCallback callback;
    private ChannelMultiplexer multiplexer;
    private int localWindowSize;
    private int remoteWindowSize = -1;
    private int totalSentSize = 0;
    private int totalSentSizeBuffered = 0;
    private int totalReceivedSize = 0;
    private int localId = -1;
    private int remoteId = -1;
    private boolean remoteClosed = false;
    private final ReadBuffer dataBuffer;
    private final ReadBuffer extendedBuffer;
    private final WriteBuffer dataWriteBuffer = new WriteBuffer();
    // TODO handle multiple extended buffer types
    private final WriteBuffer extendedWriteBuffer = new WriteBuffer();

    public Channel() {
        this(65536);
    }

    public Channel(int readBufferSize) {
        this(readBufferSize, readBufferSize);
    }

    public Channel(int readBufferSize, int extendedBufferSize) {
        this(readBufferSize, extendedBufferSize, 8192);
    }

    public Channel(int readBufferSize, int extendedBufferSize, int maxPacketSize) {
        this.dataBuffer = new ReadBuffer(readBufferSize);
        this.extendedBuffer = new ReadBuffer(extendedBufferSize);
        this.localMaxPacketSize = maxPacketSize;
        this.localWindowSize = readBufferSize;
    }

    protected int getLocalId() {
        return localId;
    }

    protected int getRemoteId() {
        return localId;
    }

    public enum Status {
        NOT_OPENED,
        OPENING,
        OPENED,
        OPEN_FAILED,
        CLOSE_FLUSHING,
        CLOSING,
        CLOSED
    }

    private Status status = Status.NOT_OPENED;
    private boolean localEof = false, remoteEof = true;

    public void eof() {
        if (status != Status.OPENED) {
            throw new IllegalStateException("EOF can only happen in status OPENED, but now is " + status.name());
        }
        if (localEof) {
            return;
        }
        localEof = true;
        for (var listener : listeners) {
            listener.onLocalEof();
        }
        
        if (!hasRemainingWriteData()) {
            callback.appendPacket(new ChannelEofPacket(remoteId));
        }
    }
    
    protected void remoteEof() {
        if (status != Status.OPENED && status != Status.CLOSE_FLUSHING) {
            throw new IllegalStateException("Remote EOF can only happen in status OPENED or CLOSE_FLUSHING, but now is " + status.name());
        }
        if (remoteClosed) {
            throw new IllegalStateException("Remote EOF cannot happen after remote closed.");
        }
        remoteEof = true;
        for (var listener : listeners) {
            listener.onRemoteEof();
        }
    }

    public boolean isLocalEof() {
        return localEof;
    }

    public boolean isRemoteEof() {
        return remoteEof;
    }

    public interface StatusListener {

        void onStatusChanged(Status status);

        void onLocalEof();

        void onRemoteEof();
    }

    private final LinkedList<StatusListener> listeners = new LinkedList<>();

    public void addListener(StatusListener listener) {
        listeners.add(listener);
    }

    private void setStatus(Status status) {
        if (this.status == status) {
            return;
        }
        this.status = status;
        for (var listener : listeners) {
            listener.onStatusChanged(status);
        }
    }

    protected ConnectionCallback getConnectionCallback() {
        return callback;
    }

    protected byte[] getOpenPayload() {
        return new byte[0];
    }

    protected final void open(ChannelMultiplexer multiplexer) {
        multiplexer.addChannel(this);
        callback.appendPacket(new ChannelOpenPacket(
                getType(),
                localId,
                localWindowSize,
                localMaxPacketSize,
                getOpenPayload()
        ));
        this.multiplexer = multiplexer;
        setStatus(Status.OPENING);
    }

    public void init(ConnectionCallback callback, int localId) {
        if (this.localId != -1) {
            throw new IllegalStateException("This channel is already bound to connection.");
        }
        this.callback = callback;
        this.localId = localId;
    }

    protected void openSucceed(int remoteId, int remoteWindowSize, int remoteMaxPacketSize, byte[] payload) {
        if (this.remoteId != -1) {
            throw new IllegalStateException("Already opened");
        }
        this.remoteId = remoteId;
        this.remoteWindowSize = remoteWindowSize;
        this.remoteMaxPacketSize = remoteMaxPacketSize;
        assert status == Status.OPENING;
        setStatus(Status.OPENED);
    }

    protected void openFailed() {
        if (status != Status.OPENING) {
            throw new IllegalStateException("Open failure can only happen in state OPENING, but now is " + status.name());
        }
        setStatus(Status.OPEN_FAILED);
        multiplexer.finishChannel(this);
    }

    public void close() {
        if (status != Status.OPENED) {
            return;
        }
        setStatus(Status.CLOSE_FLUSHING);
        try {
            flushClosing();
        } catch (IOException ex) {
            logger.log(Level.WARNING, "Closing with corrupted data", ex);
        }
    }

    private void flushClosing() throws IOException {
        if (dataBuffer.hasData()) {
            readData(dataBuffer);
            if (dataBuffer.hasData()) {
                logger.log(Level.WARNING, "Data buffer for channel {0} has remaining data, consume it.", this);
                dataBuffer.consume();
            }
        }
        if (extendedBuffer.hasData()) {
            readExtendedData(extendedBuffer);
            if (extendedBuffer.hasData()) {
                logger.log(Level.WARNING, "Extened data buffer for channel {0} has remaining data, consume it.", this);
                extendedBuffer.consume();
            }
        }
        if (dataWriteBuffer.shouldWrite()) {
            return;
        }
        if (extendedWriteBuffer.shouldWrite()) {
            return;
        }
        doClose();
    }

    protected void closed() {
        multiplexer.finishChannel(this);
    }

    protected final void doClose() {
        if (status != Status.CLOSE_FLUSHING && status != Status.OPENED) {
            throw new IllegalStateException("Sending close can only happen in status CLOSE_FLUSHING or OPENED, but now is " + status.name());
        }
        setStatus(Status.CLOSING);
        if (remoteClosed) {
            setStatus(Status.CLOSED);
            closed();
        } else {
            callback.appendPacket(new ChannelClosePacket(remoteId));
        }
    }

    protected final void receiveClose() {
        if (status != Status.CLOSING && status != Status.OPENED && status != Status.CLOSE_FLUSHING) {
            throw new IllegalStateException("Close comformation can only happen in status CLOSING or OPENED, but now is " + status.name());
        }
        switch (status) {
            case OPENED -> {
                remoteClosed = true;
                setStatus(Status.CLOSE_FLUSHING);
            }
            case CLOSE_FLUSHING -> {
                remoteClosed = true;
            }
            case CLOSING -> {
                setStatus(Status.CLOSED);
                closed();
            }
        }
    }

    protected abstract String getType();

    @Override
    public void writeChannelData(WriteContent content) throws IOException {
        if (localEof) {
            throw new IllegalStateException("Try to write data in eof state.");
        }
        totalSentSizeBuffered += content.getRemaining();
        dataWriteBuffer.append(content);
        doWriteData();
    }

    @Override
    public void writeExtendedData(WriteContent content) throws IOException {
        if (localEof) {
            throw new IllegalStateException("Try to write data in eof state.");
        }
        totalSentSizeBuffered += content.getRemaining();
        extendedWriteBuffer.append(content);
        doWriteData();
    }
    
    private boolean hasRemainingWriteData() {
        return dataWriteBuffer.shouldWrite() || extendedWriteBuffer.shouldWrite();
    }

    @Override
    public boolean shouldWrite() {
        var remoteWindowRemains = remoteWindowSizeRemains() > 0;
        return hasRemainingWriteData() && remoteWindowRemains;
    }

    private void doWriteData() throws IOException {
        while (true) {
            if (!extendedWriteBuffer.shouldWrite()) {
                break;
            }
            var avaliableWindowLen = remoteWindowSize - totalSentSize;
            if (avaliableWindowLen < 0) {
                break;
            }
            var writeLen = Math.min(avaliableWindowLen, extendedWriteBuffer.getAvaliable());
            writeLen = Math.min(remoteMaxPacketSize, writeLen);
            var packetBuf = ByteBuffer.allocate(writeLen);
            extendedWriteBuffer.doWrite(packetBuf);
            packetBuf.flip();
            callback.appendPacket(new ChannelDataPacket(remoteId, packetBuf.array()));
            totalSentSize += writeLen;
        }
        while (true) {
            if (!dataWriteBuffer.shouldWrite()) {
                break;
            }
            var avaliableRemoteWindow = remoteWindowSizeRemains();
            if (avaliableRemoteWindow <= 0) {
                break;
            }
            var writeLen = Math.min(avaliableRemoteWindow, dataWriteBuffer.getAvaliable());
            writeLen = Math.min(remoteMaxPacketSize, writeLen);
            var packetBuf = ByteBuffer.allocate(writeLen);
            dataWriteBuffer.doWrite(packetBuf);
            callback.appendPacket(new ChannelDataPacket(remoteId, packetBuf.array()));
            totalSentSize += writeLen;
        }
        if (status == Status.OPENED && localEof) {
            if (!hasRemainingWriteData()) {
                callback.appendPacket(new ChannelEofPacket(remoteId));
            }
        } else if (status == Status.CLOSE_FLUSHING) {
            if (!remoteClosed && localEof && !hasRemainingWriteData()) {
                callback.appendPacket(new ChannelEofPacket(remoteId));
            }
            flushClosing();
        }
    }

    public void enlargeRemoteWindowSize(int appendSize) {
        remoteWindowSize += appendSize;
        if (remoteUnwrittenSize() > 0) {
            callback.wakeup();
        }
    }

    private int remoteWindowSizeRemains() {
        return remoteWindowSize - totalSentSize;
    }

    private int remoteUnwrittenSize() {
        return totalSentSizeBuffered - totalSentSize;
    }

    private void enlargeLocalWindowSize(int appendSize) {
        if (appendSize < 0) {
            throw new IllegalArgumentException("Append size less than 0: " + appendSize);
        }
        localWindowSize += appendSize;
        if (appendSize == 0) {
            return;
        }
        callback.appendPacket(new ChannelWindowAdjustPacket(remoteId, appendSize));
    }

    private void adjustLocalWindow() {
        var minReadBufferRemaining = Math.min(dataBuffer.getRemaining(), extendedBuffer.getRemaining());
        var newWindowSize = totalReceivedSize + minReadBufferRemaining;
        if (localWindowSize >= newWindowSize) {
            return;
        }
        var appendSize = newWindowSize - localWindowSize;
        enlargeLocalWindowSize(appendSize);
    }

    protected final void receiveData(byte[] data) throws IOException {
        if (data.length > localMaxPacketSize) {
            throw new IOException("Received channel packet exceeds maxium packet size:"
                    + " packetSize = " + data.length
                    + ", maxSize = " + localMaxPacketSize);
        }
        var len = dataBuffer.fillData(data);
        if (len < data.length) {
            throw new IOException("Data exceeds read buffer");
        }
        while (true) {
            var privConsumed = dataBuffer.getConsumed();
            readData(dataBuffer);
            var consumedData = dataBuffer.getConsumed() - privConsumed;
            if (consumedData <= 0) {
                break;
            }
            totalReceivedSize += consumedData;
        }
        adjustLocalWindow();
    }

    protected final void receiveExtendedData(byte[] data) throws IOException {
        if (data.length > localMaxPacketSize) {
            throw new IOException("Received channel packet exceeds maxium packet size:"
                    + " packetSize = " + data.length
                    + ", maxSize = " + localMaxPacketSize);
        }
        var len = extendedBuffer.fillData(data);
        if (len < data.length) {
            throw new IOException("Extend data exceeds read buffer");
        }
        while (true) {
            var privConsumed = extendedBuffer.getConsumed();
            readData(extendedBuffer);
            if (extendedBuffer.getConsumed() == privConsumed) {
                break;
            }
            var consumedData = extendedBuffer.getConsumed() - privConsumed;
            if (consumedData <= 0) {
                break;
            }
            totalReceivedSize += consumedData;
        }
        adjustLocalWindow();
    }

    protected abstract void readData(ReadBuffer buffer) throws IOException;

    protected abstract void readExtendedData(ReadBuffer buffer) throws IOException;
    
    public interface ChannelRequestListener {
        void onRequestSucceed();
        void onRequestFailed();
    }
    
    private final LinkedList<ChannelRequestListener> requestListeners = new LinkedList<>();
    
    public final void sendRequest(ChannelRequest request, ChannelRequestListener listener) {
        var packet = new ChannelRequestPacket(
                remoteId,
                request.getName(),
                listener != null,
                request
        );
        if (listener != null) {
            requestListeners.add(listener);
        }
        callback.appendPacket(packet);
    }
    
    protected final void requestSucceed() {
        var listener = requestListeners.poll();
        if (listener == null) {
            logger.warning("Channel request succeed without listener.");
            return;
        }
        listener.onRequestSucceed();
    }
    
    protected final void requestFailed() {
        var listener = requestListeners.poll();
        if (listener == null) {
            logger.warning("Channel request failed without listener.");
            return;
        }
        listener.onRequestFailed();
    }
    
    protected final void receiveRequestPacket(boolean wantReply, ChannelRequest request) {
        boolean result = false;
        if (request != null) {
            result = receiveRequest(request);
        }
        if (!wantReply) {
            return;
        }
        if (result) {
            callback.appendPacket(new ChannelSuccessPacket(remoteId));
        } else {
            callback.appendPacket(new ChannelFailurePacket(remoteId));
        }
    }
    
    protected boolean receiveRequest(ChannelRequest request) {
        return false;
    }
}
